package il.technion.cs236369.threadPool;

import il.technion.cs236369.common.Common;
import il.technion.cs236369.exceptions.FullQueueException;

import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

/**
 * A pool of threads that holds a fixed number of threads waiting for a queue
 * Can set the maximum amount of time for a task and the maximum queue size of
 * tasks waiting to run
 * 
 * @author Gal Lalouche
 * 
 */
public class ThreadPool {
	private final int numberOfThreads;
	private final Queue<PoolTask> taskQueue;
	private final String name;

	/**
	 * Creates a new TheadPool
	 * 
	 * @param numberOfThreads
	 *            Total number of threads running (fixed)
	 * @param timeoutTime
	 *            The total amount of a time a thread is allowed to run a task
	 *            before stopping (use null for infinite)
	 * @param queueSize
	 *            The total amount of tasks that are allowed to wait (use null
	 *            for infinite)
	 * @throws IllegalArgumentException
	 *             If numberOfTheads <= 0, or if timeoutTime <= 0, or if the the
	 *             queueSize <= 0
	 * 
	 */
	public ThreadPool(int numberOfThreads, String name, Integer timeoutTime,
			Integer queueSize) {
		if (numberOfThreads <= 0) {
			throw new IllegalArgumentException(
					"numberOfThread must be a positive number");
		}
		this.name = name;

		if (queueSize == null) {
			taskQueue = new ConcurrentLinkedQueue<PoolTask>();
		} else {
			assert queueSize != null;
			taskQueue = new FiniteQueue<PoolTask>(queueSize);
		}

		if (timeoutTime != null && timeoutTime <= 0) {
			throw new IllegalArgumentException(
					"timeoutTime must be positive or null");
		}

		this.numberOfThreads = numberOfThreads;

		// create threads
		for (int i = 0; i < numberOfThreads; i++) {
			new PoolThread(this, timeoutTime, this.name + "'s Pool's thread #" + i)
					.start();
		}
	}

	/**
	 * Adds a new task
	 * 
	 * @param runnable
	 *            The task to run
	 * @throws IllegalArgumentException
	 *             if runnable is null
	 * @throws FullQueueException
	 *             if the queue is full
	 */
	public void run(Runnable runnable, Runnable timeoutRunnable)
			throws FullQueueException {
		if (runnable == null) {
			throw new IllegalArgumentException("runnable is null");
		}

		// System.err.println(taskQueue.getClass());
		// System.err.println(taskQueue);
		try {
			taskQueue.add(new PoolTask(runnable, timeoutRunnable));
		} catch (IllegalStateException e) {
			throw new FullQueueException(e);
		}
		synchronized (this) {
			notify(); // notify all threads waiting
		}
	}

	/**
	 * Adds a new task. If the queue is full, it will wait until it is empty
	 * (blocking)
	 * 
	 * @param runnable
	 *            The task to run
	 * @throws IllegalArgumentException
	 *             if runnable is null
	 */
	public void runOrWait(Runnable runnable, Runnable timeoutRunnable) {
		if (runnable == null) {
			throw new IllegalArgumentException("runnable is null");
		}

		while (true) {
			try {
				taskQueue.add(new PoolTask(runnable, timeoutRunnable));
				synchronized (this) {
					notify();
				}
				return;
			} catch (IllegalStateException e) {
				synchronized (taskQueue) {
					Common.getOut().println(
							"queue is full, waiting on " + runnable);
					try {
						taskQueue.wait();
					} catch (InterruptedException e1) {
						e1.printStackTrace();
					}
				}
				Common.getOut().println(
						"queue is waking up from sleeping on " + runnable);
			}
		}

	}

	/**
	 * Gets a new task
	 * 
	 * @return A new task or null if one doesn't exist
	 */
	PoolTask poll() {
		PoolTask task;
		task = taskQueue.poll();
		if (task != null) {
			synchronized (taskQueue) {
				taskQueue.notify();
			}
		}

		return task;
	}

	/**
	 * Gets the total number of threads
	 * 
	 * @return The total number of threads
	 */
	public int getNumberOfThreads() {
		return numberOfThreads;
	}
}
